using System;
using System.Collections.Generic;
using System.Linq;
using System.IO;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;
using Newtonsoft.Json;
using System.IO.Compression;
using ICSharpCode.SharpZipLib.BZip2;

namespace Serious
{
    public class Game1 : Game
    {
        string game_data_dname = "Serious";
        string persistent_fname = "purse.json.bz2";
        string sword_attack_fname = "sword_attack.json";

        string game_data_directory;
        string persistent_fpath { get { return Path.Combine(game_data_directory, persistent_fname); } }
        string sword_attack_fpath { get { return Path.Combine(game_data_directory, sword_attack_fname); } }

        GraphicsDeviceManager graphics;
        SpriteBatch spriteBatch;
        Effect effect;
        CullMode cull_mode = CullMode.CullCounterClockwiseFace;
        FillMode fill_mode = FillMode.Solid;
        FillMode other_fill_mode = FillMode.WireFrame;

        Random rng;

        KeyboardState prevKs;

        Texture2D crate_texture;

        List<Tuple<Cube, Cube>> cube_pairs; // [(current, previous), ...]
        Cylinder prev_player;
        Cylinder player;
        CylinderControllerBasic player_controller;
        CylinderInputHandler player_input_handler;
        CylinderFollower player_camera;

        Sword katana;
        SwordControllerAttack swing_attack;

        List<Cube> floors;
        Cube potential_floor_cube;

        IEnumerable<Cube> all_cubes { get { return cube_pairs.Select(p => p.Item1); } }
        IEnumerable<Cylinder> all_game_cylinders { get { return new Cylinder[] { player }; } }

        public Game1 ()
        {
            graphics = new GraphicsDeviceManager(this);
            Content.RootDirectory = "Content";
        }

        protected override void Initialize ()
        {
            Console.SetOut(new StreamWriter("console.log", true));
            string app_data_dir = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
            try
            {
                game_data_directory = Path.Combine(app_data_dir, game_data_dname);
                Directory.CreateDirectory(game_data_directory);
            }
            catch (IOException ex)
            {
                Logger.Log(ex.ToString());
            }



            graphics.PreferredBackBufferWidth = 1440;
            graphics.PreferredBackBufferHeight = 900;
            graphics.IsFullScreen = false;
            /*
            DisplayMode dm = GraphicsAdapter.DefaultAdapter.CurrentDisplayMode;
            graphics.PreferredBackBufferWidth = dm.Width;
            graphics.PreferredBackBufferHeight = dm.Height;
            graphics.IsFullScreen = true;
            */

            IsMouseVisible = false;
            graphics.ApplyChanges();

            Window.Title = "[?] fps";

            rng = new Random();
            prevKs = new KeyboardState();

            base.Initialize();
        }

        private game_data default_purse
        {
            get
            {
                game_data purse;

                Single l = 512.0f;      // earth length
                Single w = 512.0f;      // earth width
                Single h = 512.0f;      // earth height
                Cube earth = new Cube(
                    new Vector3(0.0f, 0.0f, -h),    // center
                    new Vector3(l, w, h),           // half-size
                    0.0f,                           // angle (degrees)
                    Vector3.Zero,                   // offset from center
                    Vector3.Zero,                   // velocity
                    0.0f,                           // angle delta (degrees)
                    "brown"                         // color name
                );
                purse.cubes = new game_data.cube[] { new game_data.cube(earth) };

                purse.player = new game_data.guy(
                    new game_data.vec3(0.0f, 0.0f, 2.0f),     // position
                    90.0f,                                    // angle (degrees)
                    0.6f,                                     // radius
                    0.75f                                     // half-height
                );
                Vector2 vangle = Vector2.Normalize(new Vector2(1.0f, 0.25f));
                purse.camera = new game_data.cam(
                    new game_data.vec2(vangle),  // vertical angle
                    19.0f,                       // distance
                    4.0f                         // z offset
                );

                return purse;
            }
        }

        private sword_swing default_sword_attack
        {
            get
            {
                float f = 30.0f;
                return new sword_swing(
                    new sword_swing.sword_state(-f, 0.0f),
                    new sword_swing.swing_frame[]{
                        new sword_swing.swing_frame(new sword_swing.sword_state(f, 0.0f), 1.0/8.0),
                        new sword_swing.swing_frame(new sword_swing.sword_state(f, 0.0f), 1.0/8.0),
                        new sword_swing.swing_frame(new sword_swing.sword_state(f, 180.0f), 1.0/8.0),

                        new sword_swing.swing_frame(new sword_swing.sword_state(f, 180.0f), 1.0/8.0),
                        new sword_swing.swing_frame(new sword_swing.sword_state(-f, 180.0f), 1.0/8.0),
                        new sword_swing.swing_frame(new sword_swing.sword_state(-f, 180.0f), 1.0/8.0),

                        new sword_swing.swing_frame(new sword_swing.sword_state(-f, 0.0f), 1.0/8.0),
                        new sword_swing.swing_frame(new sword_swing.sword_state(-f, 0.0f), 1.0/8.0),

                        new sword_swing.swing_frame(new sword_swing.sword_state(0.0f, -45.0f), 3.0/4.0),
                        new sword_swing.swing_frame(new sword_swing.sword_state(0.0f, -45.0f), 1.0/4.0),

                        new sword_swing.swing_frame(new sword_swing.sword_state(0.0f, 90.0f), 1.0/16.0),
                    }
                );
            }
        }
        
        protected override void LoadContent()
        {
            spriteBatch = new SpriteBatch(GraphicsDevice);

            effect = Content.Load<Effect>("effects");

            crate_texture = Content.Load<Texture2D>("crate");

            game_data purse;
            try
            {
                byte[] buffer;
                using (MemoryStream ms = new MemoryStream(1024)) using (FileStream file_stream = new FileStream(persistent_fpath, FileMode.Open))
                {
                    BZip2.Decompress(file_stream, ms, true);
                    buffer = ms.GetBuffer();
                }
                string json;
                using (MemoryStream ms = new MemoryStream(buffer)) using (StreamReader sr = new StreamReader(ms))
                {
                    json = sr.ReadToEnd();
                }
                purse = JsonConvert.DeserializeObject<game_data>(json);
            }
            catch (FileNotFoundException)
            {
                purse = default_purse;
            }

            SetUpGameData(purse);
        }

        private void SetUpGameData(game_data purse)
        {
            cube_pairs = new List<Tuple<Cube, Cube>>(purse.cubes.Select(c => Tuple.Create(c.as_serious_cube(), c.as_serious_cube())));

            player = purse.player.as_serious_guy();
            player_controller = new CylinderControllerBasic(player);
            player_input_handler = new CylinderInputHandler();

            prev_player = player.cloned();

            player_camera = purse.camera.as_serious_camera();
            player_camera.target = player;
            
            katana = new Sword(player, 1.5f * player.half_height);
            sword_swing attack;
            try
            {
                string json = File.ReadAllText(sword_attack_fpath);
                attack = JsonConvert.DeserializeObject<sword_swing>(json);
            }
            catch (FileNotFoundException)
            {
                attack = default_sword_attack;
                string json = JsonConvert.SerializeObject(attack);
                File.WriteAllText(sword_attack_fpath, json);
            }

            swing_attack = attack.as_serious_sword_controller_attack();
            swing_attack.subject = katana;
            floors = new List<Cube>();
            potential_floor_cube = null;
        }

        protected override void UnloadContent ()
        {
            Content.Unload();

            game_data purse;

            purse.cubes = cube_pairs.Select(p => new game_data.cube(p.Item1)).ToArray();
            purse.player = new game_data.guy(player);
            purse.camera = new game_data.cam(player_camera);



            string json = JsonConvert.SerializeObject(purse);
            byte[] buffer;
            using (MemoryStream ms = new MemoryStream())
            {
                using (StreamWriter sw = new StreamWriter(ms))
                {
                    sw.Write(json);
                }
                buffer = ms.GetBuffer();
            }
            using (MemoryStream ms = new MemoryStream(buffer)) using (FileStream fs = new FileStream(persistent_fpath, FileMode.Create, FileAccess.Write))
            {
                BZip2.Compress(ms, fs, false, 9);
            }
        }

        protected override void EndRun ()
        {
            Console.Out.Flush();

            base.EndRun();
        }



        protected override void Update(GameTime gameTime)
        {
            KeyboardEvent kev = new KeyboardEvent(Keyboard.GetState(), prevKs);
            prevKs = kev.Current;
            // Allows the game to exit
            if (kev.IsKeyDown(Keys.Escape))
            {
                Exit();
                return;
            }

            prev_player.set(player);

            if (kev.WasKeyPressed(Keys.Enter))
            {
                swing_attack.reset();
            }
            swing_attack.Update(gameTime);

            player_input_handler.HandleKeyboardEvent(kev);
            player_input_handler.Apply(player_controller);
            foreach (Tuple<Cube, Cube> cube_pair in cube_pairs)
            {
                Cube cube = cube_pair.Item1;
                Cube prev_cube = cube_pair.Item2;
                prev_cube.set(cube);
                cube.Update(gameTime);
                
                if (floors.Contains(cube))
                {
                    player.position += cube.velocity;
                    Vector2 p = player.position.AsXY();
                    p = cube.origin.AsXY() + (p - cube.origin.AsXY()).Rotated(Vector2Ext.FromDirectionDeg(cube.angle_delta));
                    player.position.X = p.X;
                    player.position.Y = p.Y;
                    player.angle += cube.angle_delta;
                }
            }
            floors.Clear();
            player.Update(gameTime);

            potential_floor_cube = null;
            foreach (Tuple<Cube, Cube> pair in cube_pairs)
            {
                Cube cube = pair.Item1;
                Cube prev_cube = pair.Item2;
                CollisionResults results = CollisionMaster.HandleCollision(ref player, cube.bounds, cube.angle, cube.origin, prev_player, prev_cube.bounds);
                if (results.floor)
                {
                    if (player.velocity.Z < -3.0f)
                    {
                        player.velocity.Z *= -0.25f;
                    }
                    else
                    {
                        player.velocity.Z = 0.0f;
                    }
                    floors.Add(cube);
                }
                if (results.intersect_xy && (cube.bounds.HiZ <= player.position.Z - player.half_height))
                {
                    if (potential_floor_cube != null)
                    {
                        if (cube.origin.Z > potential_floor_cube.origin.Z)
                        {
                            potential_floor_cube = cube;
                        }
                    }
                    else
                    {
                        potential_floor_cube = cube;
                    }
                }
            }

            if (kev.WasKeyPressed(Keys.Insert) || kev.WasKeyPressed(Keys.Home) || kev.IsKeyDown(Keys.PageUp) ||
                kev.WasKeyPressed(Keys.Delete) || kev.WasKeyPressed(Keys.End) || kev.WasKeyPressed(Keys.PageDown))
            {
                //Vector2 p2 = player.position.AsXY() + Vector2Ext.FromDirectionDeg(player.angle) * 3.0f*(d + player.radius);
                /*
                Vector2 p2 = player.position.AsXY() + Vector2Ext.FromDirectionDeg(player.angle) * 2.0f * d;
                Single z = player.position.Z - player.half_height + d;
                Vector3 p = new Vector3(p2, z);
                */

                Vector3 s = new Vector3(2.0f);

                Single angle = player.angle;

                //string color_name = BasicModel.ColoredCubes.Keys.RandomElement(rng);
                string color_name = "white";
                if (kev.WasKeyPressed(Keys.Insert))
                {
                    color_name = "yellow";
                }
                if (kev.WasKeyPressed(Keys.Home))
                {
                    color_name = "green";
                }
                if (kev.IsKeyDown(Keys.PageUp))
                {
                    color_name = "blue";
                }
                if (kev.WasKeyPressed(Keys.Delete))
                {
                    color_name = "gray";
                }
                if (kev.WasKeyPressed(Keys.End))
                {
                    color_name = "red";
                }
                if (kev.WasKeyPressed(Keys.PageDown))
                {
                    color_name = "magenta";
                }
                Vector3 offset_from_center = Vector3.Zero;
                Vector3 vel = Vector3.Zero;
                Single angle_delta = 0.0f;
                switch (color_name)
                {
                    case "red":
                        angle = player.angle - 90.0f;
                        angle_delta = 0.25f;
                        s.Z = 0.5f;
                        s *= new Vector3(2.0f, 2.0f, 1.0f);
                        offset_from_center = -s;
                        offset_from_center.Z = 0.0f;
                        break;

                    case "yellow":
                        vel = Vector3.UnitX * 0.0625f;
                        break;

                    case "green":
                        vel = Vector3.UnitY * 0.0625f;
                        break;

                    case "blue":
                        vel = Vector3.UnitZ * 0.0625f;
                        break;

                    case "magenta":
                        s.Z = 0.5f;
                        s *= new Vector3(2.0f, 2.0f, 1.0f);
                        break;
                }
                Vector2 p2 = player.position.AsXY() + Vector2Ext.FromDirectionDeg(player.angle) * 2.0f * s.X;
                Single z = player.position.Z - player.half_height + s.Z;
                Vector3 p = new Vector3(p2, z);

                Cube c = new Cube(p, s, angle, offset_from_center, vel, angle_delta, color_name);
                cube_pairs.Add(Tuple.Create(c, c.cloned()));
            }

            if (kev.WasKeyPressed(Keys.C))
            {
                cube_pairs.Clear();
                cube_pairs = default_purse.cubes.Select(c => Tuple.Create(c.as_serious_cube(), c.as_serious_cube())).ToList();
            }

            
            {
                if (kev.WasKeyPressed(Keys.NumPad0))
                {
                    Common.Swap(ref fill_mode, ref other_fill_mode);
                }
                if (kev.WasKeyPressed(Keys.NumPad1))
                {
                    cull_mode = CullMode.CullCounterClockwiseFace;
                }
                if (kev.WasKeyPressed(Keys.NumPad2))
                {
                    cull_mode = CullMode.CullClockwiseFace;
                }
                if (kev.WasKeyPressed(Keys.NumPad3))
                {
                    cull_mode = CullMode.None;
                }
            }
            if (new Random().NextDouble() >= 2.0f)
            {
                if (kev.IsKeyDown(Keys.PageUp))   player_camera.vertical_angle = player_camera.vertical_angle.Rotated(Vector2Ext.FromDirectionDeg(1.0f));
                if (kev.IsKeyDown(Keys.PageDown)) player_camera.vertical_angle = player_camera.vertical_angle.Rotated(Vector2Ext.FromDirectionDeg(-1.0f));

                if (kev.IsKeyDown(Keys.Home)) player_camera.dist -= 0.75f;
                if (kev.IsKeyDown(Keys.End))  player_camera.dist += 0.75f;

                if (kev.IsKeyDown(Keys.Insert)) player_camera.z_offset += 0.25f;
                if (kev.IsKeyDown(Keys.Delete)) player_camera.z_offset -= 0.25f;
            }

            {
                if (kev.IsKeyDown(Keys.NumPad9)) player_camera.vertical_angle = player_camera.vertical_angle.Rotated(Vector2Ext.FromDirectionDeg(1.0f));
                if (kev.IsKeyDown(Keys.NumPad6)) player_camera.vertical_angle = player_camera.vertical_angle.Rotated(Vector2Ext.FromDirectionDeg(-1.0f));

                if (kev.IsKeyDown(Keys.NumPad8)) player_camera.dist -= 0.75f;
                if (kev.IsKeyDown(Keys.NumPad5)) player_camera.dist += 0.75f;

                if (kev.IsKeyDown(Keys.NumPad7)) player_camera.z_offset += 0.25f;
                if (kev.IsKeyDown(Keys.NumPad4)) player_camera.z_offset -= 0.25f;
            }

            base.Update(gameTime);
        }

        int frames = 0;
        TimeSpan timeSinceLastCount = TimeSpan.Zero;
        protected override void Draw(GameTime gameTime)
        {
            timeSinceLastCount += gameTime.ElapsedGameTime;
            if (timeSinceLastCount > TimeSpan.FromSeconds(1.0))
            {
                Window.Title = frames + " fps";
                timeSinceLastCount = TimeSpan.Zero;
                frames = 0;
            }
            frames++;

            GraphicsDevice.Clear(ClearOptions.Target | ClearOptions.DepthBuffer, Color.Black, 1.0f, 0);

            RasterizerState rs = new RasterizerState();
            rs.CullMode = cull_mode;
            rs.FillMode = fill_mode;
            GraphicsDevice.RasterizerState = rs;
            //GraphicsDevice.BlendState = BlendState.AlphaBlend;


            effect.CurrentTechnique = effect.Techniques["Colored"];

            Matrix proj_matrix = Matrix.CreatePerspectiveFieldOfView(MathHelper.PiOver4, GraphicsDevice.Viewport.AspectRatio, 0.0625f, 10000.0f);
            effect.Parameters["xView"].SetValue(player_camera.ViewMatrix);
            effect.Parameters["xProjection"].SetValue(proj_matrix);

            Vector3 lightDirection = player_camera.to - player_camera.from;
            effect.Parameters["xLightDirection"].SetValue(Vector3.Normalize(lightDirection));
            effect.Parameters["xAmbient"].SetValue(0.25f);
            effect.Parameters["xEnableLighting"].SetValue(true);

            foreach (Cylinder c in all_game_cylinders)
            {
                effect.Parameters["xWorld"].SetValue(c.transformation);
                foreach (EffectPass pass in effect.CurrentTechnique.Passes)
                {
                    pass.Apply();
                    c.Draw(gameTime, GraphicsDevice);
                }
            }

            Sword s = katana;
            {
                Matrix parent_transformation = s.parent.transformation;
                effect.Parameters["xWorld"].SetValue(s.transformation * parent_transformation);
                foreach (EffectPass pass in effect.CurrentTechnique.Passes)
                {
                    pass.Apply();
                    s.Draw(gameTime, GraphicsDevice);
                }
            }

            if (potential_floor_cube != null)
            {
                Single prev_ambient = effect.Parameters["xAmbient"].GetValueSingle();
                Vector3 prev_light_direction = effect.Parameters["xLightDirection"].GetValueVector3();
                {
                    effect.Parameters["xAmbient"].SetValue(0.0f);
                    effect.Parameters["xLightDirection"].SetValue(Vector3.Zero);

                    Vector3 sc = new Vector3(1.0f, 1.0f, 0.0f);
                    Matrix msc = Matrix.CreateScale(sc);
                    Vector3 p = new Vector3(0.0f, 0.0f, potential_floor_cube.bounds.HiZ + 1.0f / 32.0f);
                    Matrix mp = Matrix.CreateTranslation(p);
                    Matrix mm = msc * mp;
                    effect.Parameters["xWorld"].SetValue(player.transformation * mm);
                    foreach (EffectPass pass in effect.CurrentTechnique.Passes)
                    {
                        pass.Apply();
                        player.Draw(gameTime, GraphicsDevice);
                    }

                    effect.Parameters["xWorld"].SetValue(s.transformation * s.parent.transformation * mm);
                    //effect.Parameters["xWorld"].SetValue(s.transformation * parent_transformation);
                    foreach (EffectPass pass in effect.CurrentTechnique.Passes)
                    {
                        pass.Apply();
                        s.Draw(gameTime, GraphicsDevice);
                    }
                }
                effect.Parameters["xAmbient"].SetValue(prev_ambient);
                effect.Parameters["xLightDirection"].SetValue(prev_light_direction);
            }


            effect.CurrentTechnique = effect.Techniques["Textured"];
            effect.Parameters["xTexture"].SetValue(crate_texture);

            effect.Parameters["xView"].SetValue(player_camera.ViewMatrix);
            effect.Parameters["xProjection"].SetValue(proj_matrix);

            effect.Parameters["xLightDirection"].SetValue(Vector3.Normalize(lightDirection));
            effect.Parameters["xAmbient"].SetValue(0.25f);
            effect.Parameters["xEnableLighting"].SetValue(true);

            foreach (Cube cube in all_cubes)
            {
                effect.Parameters["xWorld"].SetValue(cube.transformation);
                foreach (EffectPass pass in effect.CurrentTechnique.Passes)
                {
                    pass.Apply();
                    cube.Draw(gameTime, GraphicsDevice);
                }
            }


            base.Draw(gameTime);
        }
    }
}
    